using Indiefreaks.Xna.Core;
using Indiefreaks.Xna.Logic;
using Indiefreaks.Xna.Rendering.Camera;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace Indiefreaks.AceOnSteroids.Logic.Behaviors
{
    /// <summary>
    /// This behavior will take care of positionning the game entity on the other side of the screen when it reaches one of the borders of the Screen as you would expect
    /// in an Asteroids clone.
    /// </summary>
    public class WrappedScreenBehavior : Behavior
    {
        private ICamera _camera;
        private Vector3 _screenPosition;
        private bool _shouldWrap;
        private Viewport _viewport;

        /// <summary>
        /// Creates a new instance
        /// </summary>
        public WrappedScreenBehavior()
        {
            // we don't need to execute this behavior on every update
            // Each Behavior can specify how many times per frame it will execute allowing us to optimize performances
            // This behavior shouldn't be too expensive CPU wise but it illustrates the usage of the Frequency property
            // we'll set this behavior to run once per frame only (for a 60 fps, it's 1/60)
            Frequency = ExecutionFrequency.PartialUpdate1Hz;

            // this behavior doesn't need to execute when the attached game entity is inside the screen boudaries
            // we can use Condition delegates to tell the Behavior when to execute its Commands. You can use as many Conditions as you want.
            AddCondition(IsOutOfView);

            // we'll need to compute the current ParentObject position on screen to later check which side of the screen it is leaving.
            // we'll therefore use a local command for that
            AddLocalCommand(ComputeScreenPosition);

            // we now need to find which side of the screen the game entity is moving towards. We'll use 4 local commands for each side of the screen and use a condition to
            // define if it should execute.
            AddLocalCommand(() => _screenPosition.X < _viewport.Bounds.Left - 100, MoveEntityToTheRight);
                // we check if the computed screen position X value is on the left side of the viewport. We add a constant 100 value to make sure it is really out of view
            AddLocalCommand(() => _screenPosition.X > _viewport.Bounds.Right + 100, MoveEntityToTheLeft);
            AddLocalCommand(() => _screenPosition.Y < _viewport.Bounds.Top - 100, MoveEntityToTheBottom);
            AddLocalCommand(() => _screenPosition.Y > _viewport.Bounds.Bottom + 100, MoveEntityToTheTop);

            // now that our screen position is set, if so, we transform the game entity position in 3D using the newly created Screen position
            AddLocalCommand(() => _shouldWrap, Compute3DPosition);
        }

        /// <summary>
        /// Condition delegate which looks if the current game entity is inside the camera view
        /// </summary>
        /// <returns/>
        private bool IsOutOfView()
        {
            // we retrieve the current camera instance
            _camera = Application.SunBurn.GetManager<CameraManager>(true).ActiveCamera;

            // and we test the ParentObject's World BoundingBox against the camera view frustrum.
            return _camera.SceneState.ViewFrustum.Contains(Agent.ParentObject.WorldBoundingBox) == ContainmentType.Disjoint;
        }

        /// <summary>
        /// Projects the current game entity world position to its screen position
        /// </summary>
        /// <param name="command"/>
        /// <returns/>
        private object ComputeScreenPosition(Command command)
        {
            // we retrieve the current Viewport
            _viewport = Application.Graphics.GraphicsDevice.Viewport;

            // and we project the current ParentObject Position using the camera Projection and View matrices
            _screenPosition = _viewport.Project(Agent.ParentObject.World.Translation, _camera.SceneState.Projection, _camera.SceneState.View, Matrix.Identity);

            // since this is a local command only, we return null
            return null;
        }

        private object MoveEntityToTheLeft(Command command)
        {
            // we set the new screen position of the game entity to the left and set the _shouldWrap value to true so that we can tell another command to apply the Screenposition to the game entity in 3d coordinates
            _screenPosition.X = _viewport.Bounds.Left - 100;
            _shouldWrap = true;

            // still a local command only
            return null;
        }

        private object MoveEntityToTheRight(Command command)
        {
            _screenPosition.X = _viewport.Bounds.Right + 100;
            _shouldWrap = true;

            return null;
        }

        private object MoveEntityToTheTop(Command command)
        {
            _screenPosition.Y = _viewport.Bounds.Top - 100;
            _shouldWrap = true;

            return null;
        }

        private object MoveEntityToTheBottom(Command command)
        {
            _screenPosition.Y = _viewport.Bounds.Bottom + 100;
            _shouldWrap = true;

            return null;
        }

        /// <summary>
        /// Unprojects the new game entity screen position to its world 3d transform.
        /// </summary>
        /// <param name="command"/>
        /// <returns/>
        private object Compute3DPosition(Command command)
        {
            // here we'll do a bit of maths ;)
            float x = _screenPosition.X;
            float y = _screenPosition.Y;

            Vector3 nearSource = _viewport.Unproject(new Vector3(x, y, _viewport.MinDepth), _camera.SceneState.Projection, _camera.SceneState.View, Matrix.Identity);
            Vector3 farSource = _viewport.Unproject(new Vector3(x, y, _viewport.MaxDepth), _camera.SceneState.Projection, _camera.SceneState.View, Matrix.Identity);

            Vector3 direction = farSource - nearSource;
            direction.Normalize();

            var ray = new Ray(nearSource, direction);
            var plane = new Plane(Vector3.Up, 0f);

            float denominator = Vector3.Dot(plane.Normal, ray.Direction);
            float numerator = Vector3.Dot(plane.Normal, ray.Position) + plane.D;
            float t = -(numerator/denominator);

            Matrix previousWorldTransform = Agent.ParentObject.World;
            previousWorldTransform.Translation = nearSource + direction*t;
            Agent.ParentObject.World = previousWorldTransform;

            // we don't forget to set the _shouldWrap back to false
            _shouldWrap = false;

            return null;
        }
    }
}